﻿using System;
using System.Collections.Generic;
using MetaCollection.Framework.Model;

namespace MetaCollection.Framework.Change
{
	/// <summary>
	/// A set of flags that indicate the type of change an IItemChange represents.
	/// </summary>
	[Flags]
	public enum ItemChangeTypes
	{
		/// <summary>
		/// No changes detected
		/// </summary>
		None = 0,
		/// <summary>
		/// Unknown indicates that a change is detected but no more specific ItemChangeTypes could be assigned.
		/// </summary>
		Unknown = 1,

		Create = 1 << 1,
		Delete = 1 << 2,
		Version = 1 << 3,
		Publish = 1 << 4,
		Hierarchy = 1 << 5,

		PropertyAdd = 1 << 6,
		PropertyModify = 1 << 7,
		PropertyDelete = 1 << 8,

		RelationshipAdd = 1 << 9,
		RelationshipModify = 1 << 10,
		RelationshipDelete = 1 << 11,

		PermissionAdd = 1 << 12,
		PermissionModify = 1 << 13,
		PermissionDelete = 1 << 14,

		/// <summary>
		/// All types of permission changess
		/// </summary>
		PermissionTypes = PermissionAdd | PermissionModify | PermissionDelete,
		/// <summary>
		/// All types of property collection changes
		/// </summary>
		PropertyTypes = PropertyAdd | PropertyModify | PropertyDelete,
		/// <summary>
		/// All types of relationship changes
		/// </summary>
		RelationshipTypes = RelationshipAdd | RelationshipModify | RelationshipDelete,
		/// <summary>
		/// All change types (including unknown)
		/// </summary>
		AllTypes = Unknown | Create | Delete | Version | Hierarchy | PropertyTypes | RelationshipTypes | PermissionTypes

	}

	public static class ItemChangeTypeExtension
	{
		/// <summary>
		/// Determines if a permission check is possible for this set of change types.
		/// </summary>
		/// <remarks>If there are unknown changes then this method will return false.</remarks>
		/// <param name="itemChangeType">Type of the item change.</param>
		/// <returns>
		/// 	<c>true</c> if this instance [can check permissions] the specified item change type; otherwise, <c>false</c>.
		/// </returns>
		public static bool CanCheckPermissions(this ItemChangeTypes itemChangeType)
		{
			return !((itemChangeType & ItemChangeTypes.Unknown) == ItemChangeTypes.Unknown);
		}

		/// <summary>
		/// Determines the required AccessRights for a set of ItemChangeTypes on the Item
		/// </summary>
		/// <remarks>Hierarchy and Create changes required the AccessRights to be checked against the item parent</remarks>
		/// <param name="itemChangeType">Type of the item change.</param>
		/// <returns>AccessRights required for the change.</returns>
		public static AccessRights RequiredRightsOnItem(this ItemChangeTypes itemChangeType)
		{
			AccessRights req = AccessRights.None;
			if (itemChangeType == ItemChangeTypes.None)
				return req;

			if ((itemChangeType & ItemChangeTypes.Unknown) == ItemChangeTypes.Unknown)
				req = req | AccessRights.Unknown;
			if ((itemChangeType & ItemChangeTypes.Delete) == ItemChangeTypes.Delete)
				req = req | AccessRights.DeleteItem;
			if ((itemChangeType & ItemChangeTypes.Version) == ItemChangeTypes.Version)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.Publish) == ItemChangeTypes.Publish)
				req = req | AccessRights.ModifyPublishedVersion;
			if ((itemChangeType & ItemChangeTypes.Hierarchy) == ItemChangeTypes.Hierarchy)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.PropertyAdd) == ItemChangeTypes.PropertyAdd)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.PropertyModify) == ItemChangeTypes.PropertyModify)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.PropertyDelete) == ItemChangeTypes.PropertyDelete)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.RelationshipAdd) == ItemChangeTypes.RelationshipAdd)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.RelationshipModify) == ItemChangeTypes.RelationshipModify)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.RelationshipDelete) == ItemChangeTypes.RelationshipDelete)
				req = req | AccessRights.WriteProperties;
			if ((itemChangeType & ItemChangeTypes.PermissionAdd) == ItemChangeTypes.PermissionAdd)
				req = req | AccessRights.WriteItemClaims;
			if ((itemChangeType & ItemChangeTypes.PermissionModify) == ItemChangeTypes.PermissionModify)
				req = req | AccessRights.WriteItemClaims;
			if ((itemChangeType & ItemChangeTypes.PermissionDelete) == ItemChangeTypes.PermissionDelete)
				req = req | AccessRights.WriteItemClaims;
			return req;
		}

		/// <summary>
		/// Gets the enumerator for the ItemChangeTypes instance.
		/// </summary>
		/// <param name="itemChangeType">Type of the item change.</param>
		/// <returns>an enumerator for the set flags in the ItemChangeTypes instance</returns>
		public static IEnumerable<ItemChangeTypes> GetEnumerator(this ItemChangeTypes itemChangeType)
		{
			if (itemChangeType == ItemChangeTypes.None)
				yield break;

			if ((itemChangeType & ItemChangeTypes.Unknown) == ItemChangeTypes.Unknown)
				yield return ItemChangeTypes.Unknown;
			if ((itemChangeType & ItemChangeTypes.Delete) == ItemChangeTypes.Delete)
				yield return ItemChangeTypes.Delete;
			if ((itemChangeType & ItemChangeTypes.Version) == ItemChangeTypes.Version)
				yield return ItemChangeTypes.Version;
			if ((itemChangeType & ItemChangeTypes.Publish) == ItemChangeTypes.Publish)
				yield return ItemChangeTypes.Publish;
			if ((itemChangeType & ItemChangeTypes.Hierarchy) == ItemChangeTypes.Hierarchy)
				yield return ItemChangeTypes.Hierarchy;
			if ((itemChangeType & ItemChangeTypes.PropertyAdd) == ItemChangeTypes.PropertyAdd)
				yield return ItemChangeTypes.PropertyAdd;
			if ((itemChangeType & ItemChangeTypes.PropertyModify) == ItemChangeTypes.PropertyModify)
				yield return ItemChangeTypes.PropertyModify;
			if ((itemChangeType & ItemChangeTypes.PropertyDelete) == ItemChangeTypes.PropertyDelete)
				yield return ItemChangeTypes.PropertyDelete;
			if ((itemChangeType & ItemChangeTypes.RelationshipAdd) == ItemChangeTypes.RelationshipAdd)
				yield return ItemChangeTypes.RelationshipAdd;
			if ((itemChangeType & ItemChangeTypes.RelationshipModify) == ItemChangeTypes.RelationshipModify)
				yield return ItemChangeTypes.RelationshipModify;
			if ((itemChangeType & ItemChangeTypes.RelationshipDelete) == ItemChangeTypes.RelationshipDelete)
				yield return ItemChangeTypes.RelationshipDelete;
			if ((itemChangeType & ItemChangeTypes.PermissionAdd) == ItemChangeTypes.PermissionAdd)
				yield return ItemChangeTypes.PermissionAdd;
			if ((itemChangeType & ItemChangeTypes.PermissionModify) == ItemChangeTypes.PermissionModify)
				yield return ItemChangeTypes.PermissionModify;
			if ((itemChangeType & ItemChangeTypes.PermissionDelete) == ItemChangeTypes.PermissionDelete)
				yield return ItemChangeTypes.PermissionDelete;
			yield break;
		}

		/// <summary>
		///  Determines the required AccessRights for a set of ItemChangeTypes on the Item's Parent
		/// </summary>
		/// <param name="itemChangeType">Type of the item change.</param>
		/// <returns>A set of AccessRights to demand of the Item's Parent</returns>
		public static AccessRights RequiredRightsOnParent(this ItemChangeTypes itemChangeType)
		{
			AccessRights req = AccessRights.None;
			if (itemChangeType == ItemChangeTypes.None)
				return req;
			if ((itemChangeType & ItemChangeTypes.Create) == ItemChangeTypes.Create)
				req = req | AccessRights.Append;
			if ((itemChangeType & ItemChangeTypes.Hierarchy) == ItemChangeTypes.Hierarchy)
				req = req | AccessRights.Append;
			return req;
		}

		/// <summary>
		/// Gets the type of the change based on which field or property in the ItemInstance is being
		/// evaluated.  For any property which does not have a specific change type the Unknown change type
		/// is assigned.
		/// </summary>
		/// <param name="itemChangeType">Type of the item change.</param>
		/// <param name="propertyName">Name of the property.</param>
		/// <param name="oldValue">The old value.</param>
		/// <param name="newValue">The new value.</param>
		/// <returns>The ItemChangeTypes that apply to this change.</returns>
		public static ItemChangeTypes UpdateChangeType(this ItemChangeTypes itemChangeType, string propertyName, object oldValue, object newValue)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName");

			var changes = itemChangeType;
			var changePrefix = String.Empty;

			UpdateChangePrefix(propertyName, ref changePrefix);
			
			if (!String.IsNullOrEmpty(changePrefix))
			{
				if (!IsNullOrEmpty(oldValue) && !IsNullOrEmpty(newValue))
				{
					if (!Equality(oldValue, newValue))
						changes = changes | (ItemChangeTypes)Enum.Parse(typeof(ItemChangeTypes), changePrefix + "Modify");
				}
				else if (IsNullOrEmpty(oldValue) != IsNullOrEmpty(newValue)) // if they are not both null then we need an add or a delete
				{
					if (propertyName.Contains("].")) // this indicates that we are dealing with a property on a collection element.  You can't delete/add these only modify.
						changes = changes | (ItemChangeTypes)Enum.Parse(typeof(ItemChangeTypes), changePrefix + "Modify");
					else if (!IsNullOrEmpty(newValue)) // old null, added
						changes = changes | (ItemChangeTypes)Enum.Parse(typeof(ItemChangeTypes), changePrefix + "Add");
					else // new null, deleted
						changes = changes | (ItemChangeTypes)Enum.Parse(typeof(ItemChangeTypes), changePrefix + "Delete");
				}
			}
			else // we are not working with one of the 3 collections.  properties in this branch are on IItemInstance itself.
			{
				if (propertyName.Contains("Version") && !Equality(oldValue, newValue)) // Version, LeftVersion, RightVersion
					changes = changes | ItemChangeTypes.Version;
				else if ((propertyName.Contains("Id") || propertyName.Contains("Name")) && !Equality(oldValue, newValue))
					changes = changes | ItemChangeTypes.Hierarchy;
				else if (propertyName.Contains("Published") && !Equality(oldValue, newValue))
					changes = changes | ItemChangeTypes.Publish;
				else if (!Equality(oldValue, newValue))
					changes = changes | ItemChangeTypes.Version; // createdby, comment, mimetype, resourceprovider, lastmodified
			}
			return changes;
		}

		/// <summary>
		/// Determines whether the specified request type has type.
		/// </summary>
		/// <param name="requestType">Type of the request.</param>
		/// <param name="typeToMatch">The type to match.</param>
		/// <returns>
		/// 	<c>true</c> if the specified request type has type; otherwise, <c>false</c>.
		/// </returns>
		public static bool HasType(this ItemChangeTypes changeType, ItemChangeTypes typeToMatch)
		{
			return (changeType & typeToMatch) == typeToMatch;
		}

		/// <summary>
		/// Determines whether the current instance of ItemChangeTypes has any of the typesToMatch.
		/// </summary>
		/// <param name="changeType">Type of the change.</param>
		/// <param name="typesToMatch">The types to match.</param>
		/// <returns>
		/// 	<c>true</c> if the current instance has any of the types to match otherwise, <c>false</c>.
		/// </returns>
		public static bool HasAnyType(this ItemChangeTypes changeType, ItemChangeTypes typesToMatch)
		{
			IEnumerable<ItemChangeTypes> types = typesToMatch.GetEnumerator();
			foreach (var type in types)
			{
				if (changeType.HasType(type))
					return true;
			}
			return false;
		}

		/// <summary>
		/// Updates the change prefix for parsing an enumaration value.
		/// </summary>
		/// <param name="propertyName">Name of the property.</param>
		/// <param name="changePrefix">The change prefix.</param>
		private static void UpdateChangePrefix(string propertyName, ref string changePrefix)
		{
			if (propertyName.StartsWith("IItemInstance.Properties", StringComparison.Ordinal))
				changePrefix = "Property";
			else if (propertyName.StartsWith("IItemInstance.Relationships", StringComparison.Ordinal))
				changePrefix = "Relationship";
			else if (propertyName.StartsWith("IItemInstance.AccessClaims", StringComparison.Ordinal))
				changePrefix = "Permission";
		}

		/// <summary>
		/// Determines whether [is null or empty] [the specified value].
		/// </summary>
		/// <remarks>This special test is here because an empty string and a null string are (or should be) treated as equal.</remarks>
		/// <param name="value">The value to test</param>
		/// <returns>
		/// 	<c>true</c> if [is null or empty] [the specified value]; otherwise, <c>false</c>.
		/// </returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification="Due to the way this is testing for null this is not an unnecessary cast")]
		private static bool IsNullOrEmpty(object value)
		{
			if (value == null)
				return true;
			else
			{
				if (value is string)
					return string.IsNullOrEmpty(value as string);
				else if (value is IItemProperty)
					return string.IsNullOrEmpty((value as IItemProperty).KeyValue);
				else
					return false;
			}
		}

		/// <summary>
		/// Determines the Equality of the specified values.
		/// </summary>
		/// <param name="oldValue">The old value.</param>
		/// <param name="newValue">The new value.</param>
		/// <returns>true if a string representation of the two values is equal.</returns>
		private static bool Equality(object oldValue, object newValue)
		{
			string oldString = (IsNullOrEmpty(oldValue) ? String.Empty : oldValue.ToString());
			string newString = (IsNullOrEmpty(newValue) ? String.Empty : newValue.ToString());
			return oldString.Equals(newString, StringComparison.OrdinalIgnoreCase);
		}
	}
}
